// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:io' show Platform;

import 'package:path/path.dart' as p;
import 'package:pigeon/generator_tools.dart';
import 'package:pigeon/pigeon.dart';

import 'process_utils.dart';

enum GeneratorLanguages {
  cpp,
  java,
  kotlin,
  objc,
  swift,
}

// A map of pigeons/ files to the languages that they can't yet be generated
// for due to limitations of that generator.
const Map<String, Set<GeneratorLanguages>> _unsupportedFiles =
    <String, Set<GeneratorLanguages>>{};

String _snakeToPascalCase(String snake) {
  final List<String> parts = snake.split('_');
  return parts
      .map((String part) =>
          part.substring(0, 1).toUpperCase() + part.substring(1))
      .join();
}

// Remaps some file names for Java output, since the filename on Java will be
// the name of the generated top-level class. In some cases this is necessary
// (e.g., "list", which collides with the Java List class in tests), and in
// others it is just preserving previous behavior from the earlier Bash version
// of the generation to minimize churn during the migration.
// TODO(stuartmorgan): Remove the need for this when addressing
// https://github.com/flutter/flutter/issues/115168.
String _javaFilenameForName(String inputName) {
  const Map<String, String> specialCases = <String, String>{
    'message': 'MessagePigeon',
  };
  return specialCases[inputName] ?? _snakeToPascalCase(inputName);
}

Future<int> generateExamplePigeons() async {
  return runPigeon(
    input: './example/app/pigeons/messages.dart',
    basePath: './example/app',
    suppressVersion: true,
  );
}

Future<int> generateTestPigeons({required String baseDir}) async {
  // TODO(stuartmorgan): Make this dynamic rather than hard-coded. Or eliminate
  // it entirely; see https://github.com/flutter/flutter/issues/115169.
  const List<String> inputs = <String>[
    'background_platform_channels',
    'core_tests',
    'enum',
    'flutter_unittests', // Only for Dart unit tests in shared_test_plugin_code
    'message',
    'multiple_arity',
    'non_null_fields',
    'null_fields',
    'nullable_returns',
    'primitive',
  ];

  final String outputBase = p.join(baseDir, 'platform_tests', 'test_plugin');
  final String alternateOutputBase =
      p.join(baseDir, 'platform_tests', 'alternate_language_test_plugin');
  final String sharedDartOutputBase =
      p.join(baseDir, 'platform_tests', 'shared_test_plugin_code');

  for (final String input in inputs) {
    final String pascalCaseName = _snakeToPascalCase(input);
    final Set<GeneratorLanguages> skipLanguages =
        _unsupportedFiles[input] ?? <GeneratorLanguages>{};

    // Generate the default language test plugin output.
    int generateCode = await runPigeon(
      input: './pigeons/$input.dart',
      dartOut: '$sharedDartOutputBase/lib/src/generated/$input.gen.dart',
      // Android
      kotlinOut: skipLanguages.contains(GeneratorLanguages.kotlin)
          ? null
          : '$outputBase/android/src/main/kotlin/com/example/test_plugin/$pascalCaseName.gen.kt',
      kotlinPackage: 'com.example.test_plugin',
      kotlinErrorClassName:
          input == 'core_tests' ? null : '${pascalCaseName}Error',
      // iOS
      swiftOut: skipLanguages.contains(GeneratorLanguages.swift)
          ? null
          : '$outputBase/ios/Classes/$pascalCaseName.gen.swift',
      // Windows
      cppHeaderOut: skipLanguages.contains(GeneratorLanguages.cpp)
          ? null
          : '$outputBase/windows/pigeon/$input.gen.h',
      cppSourceOut: skipLanguages.contains(GeneratorLanguages.cpp)
          ? null
          : '$outputBase/windows/pigeon/$input.gen.cpp',
      cppNamespace: '${input}_pigeontest',
      suppressVersion: true,
      dartPackageName: 'pigeon_integration_tests',
    );
    if (generateCode != 0) {
      return generateCode;
    }

    // macOS has to be run as a separate generation, since currently Pigeon
    // doesn't have a way to output separate macOS and iOS Swift output in a
    // single invocation.
    generateCode = await runPigeon(
      input: './pigeons/$input.dart',
      swiftOut: skipLanguages.contains(GeneratorLanguages.swift)
          ? null
          : '$outputBase/macos/Classes/$pascalCaseName.gen.swift',
      suppressVersion: true,
      dartPackageName: 'pigeon_integration_tests',
    );
    if (generateCode != 0) {
      return generateCode;
    }

    // Generate the alternate language test plugin output.
    generateCode = await runPigeon(
      input: './pigeons/$input.dart',
      // Android
      // This doesn't use the '.gen' suffix since Java has strict file naming
      // rules.
      javaOut: skipLanguages.contains(GeneratorLanguages.java)
          ? null
          : '$alternateOutputBase/android/src/main/java/com/example/'
              'alternate_language_test_plugin/${_javaFilenameForName(input)}.java',
      javaPackage: 'com.example.alternate_language_test_plugin',
      // iOS
      objcHeaderOut: skipLanguages.contains(GeneratorLanguages.objc)
          ? null
          : '$alternateOutputBase/ios/Classes/$pascalCaseName.gen.h',
      objcSourceOut: skipLanguages.contains(GeneratorLanguages.objc)
          ? null
          : '$alternateOutputBase/ios/Classes/$pascalCaseName.gen.m',
      suppressVersion: true,
      dartPackageName: 'pigeon_integration_tests',
    );
    if (generateCode != 0) {
      return generateCode;
    }

    // macOS has to be run as a separate generation, since currently Pigeon
    // doesn't have a way to output separate macOS and iOS Swift output in a
    // single invocation.
    generateCode = await runPigeon(
      input: './pigeons/$input.dart',
      objcHeaderOut: skipLanguages.contains(GeneratorLanguages.objc)
          ? null
          : '$alternateOutputBase/macos/Classes/$pascalCaseName.gen.h',
      objcSourceOut: skipLanguages.contains(GeneratorLanguages.objc)
          ? null
          : '$alternateOutputBase/macos/Classes/$pascalCaseName.gen.m',
      suppressVersion: true,
      dartPackageName: 'pigeon_integration_tests',
    );
    if (generateCode != 0) {
      return generateCode;
    }
  }
  return 0;
}

Future<int> runPigeon({
  required String input,
  String? kotlinOut,
  String? kotlinPackage,
  String? kotlinErrorClassName,
  String? swiftOut,
  String? cppHeaderOut,
  String? cppSourceOut,
  String? cppNamespace,
  String? dartOut,
  String? dartTestOut,
  String? javaOut,
  String? javaPackage,
  String? objcHeaderOut,
  String? objcSourceOut,
  String objcPrefix = '',
  bool suppressVersion = false,
  String copyrightHeader = './copyright_header.txt',
  String? basePath,
  String? dartPackageName,
}) async {
  // Temporarily suppress the version output via the global flag if requested.
  // This is done because having the version in all the generated test output
  // means every version bump updates every test file, which is problematic in
  // review. For files where CI validates that this generation is up to date,
  // having the version in these files isn't useful.
  // TODO(stuartmorgan): Remove the option and do this unconditionally once
  // all the checked in files are being validated; currently only
  // generatePigeons is being checked in CI.
  final bool originalWarningSetting = includeVersionInGeneratedWarning;
  if (suppressVersion) {
    includeVersionInGeneratedWarning = false;
  }
  final int result = await Pigeon.runWithOptions(PigeonOptions(
    input: input,
    copyrightHeader: copyrightHeader,
    dartOut: dartOut,
    dartTestOut: dartTestOut,
    dartOptions: const DartOptions(),
    cppHeaderOut: cppHeaderOut,
    cppSourceOut: cppSourceOut,
    cppOptions: CppOptions(namespace: cppNamespace),
    javaOut: javaOut,
    javaOptions: JavaOptions(package: javaPackage),
    kotlinOut: kotlinOut,
    kotlinOptions: KotlinOptions(
        package: kotlinPackage, errorClassName: kotlinErrorClassName),
    objcHeaderOut: objcHeaderOut,
    objcSourceOut: objcSourceOut,
    objcOptions: ObjcOptions(prefix: objcPrefix),
    swiftOut: swiftOut,
    swiftOptions: const SwiftOptions(),
    basePath: basePath,
    dartPackageName: dartPackageName,
  ));
  includeVersionInGeneratedWarning = originalWarningSetting;
  return result;
}

/// Runs the repository tooling's format command on this package.
///
/// This is intended for formatting generated output, but since there's no
/// way to filter to specific files in with the repo tooling it runs over the
/// entire package.
Future<int> formatAllFiles({required String repositoryRoot}) {
  final String dartCommand = Platform.isWindows ? 'dart.exe' : 'dart';
  return runProcess(
      dartCommand,
      <String>[
        'run',
        'script/tool/bin/flutter_plugin_tools.dart',
        'format',
        '--packages=pigeon',
      ],
      streamOutput: false,
      workingDirectory: repositoryRoot,
      logFailure: true);
}
